﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using VulnDBExplorer.Framework.Impl;
using Winista.Text.HtmlParser;
using Winista.Text.HtmlParser.Lex;
using Winista.Text.HtmlParser.Tags;
using System.Diagnostics;
using System.IO;
using Luna;
using System.Threading;

namespace VulnDB.Action.DataCollectors.Actions {
	class FirefoxTestSuiteCollectorAction: DataCollectorAction {

		public string TestSuitURL { get; set; }

		public string QueryArguments { get; set; }

		public string Output { get; set; }

		public int ItemPerPage { get; set; }

		public int StartingPage { get; set; }

		public int NumberOfWorker { get; set; }

		public FirefoxTestSuiteCollectorAction() {
			this.Name = "FirefoxTestSuiteCollectorAction";
			this.Text = "Collect Firefox TestSuite";

			this.ExecutionMode = ActionExecutionMode.SeparatedThread;
			TestSuitURL = "https://litmus.mozilla.org/advanced_search.cgi?product=1&limit={0}&automated=all&withbugs=all";
			Output = "ff-testsuit.cvs";
			ItemPerPage = 15;
			NumberOfWorker = 1;
			StartingPage = 1;
			PageCount = int.MaxValue;
		}

		public int PageCount { get; set; }

		public int ProcessedPages { get; set; }

		public int TestResultCount { get; set; }

		List<TestSuiteCollector> workers;

		private StreamWriter output;

		protected override void onExecute(object sender, Luna.Framework.ActionExecutionArgs args) {
			Console.log("Collecting Firefox TestSuite...");
			ProgressMonitor = Application.createProgressMonitor();
			ProgressMonitor.start("Collecting test suite...", int.MaxValue);
			base.onExecute(sender, args);

			workers = new List<TestSuiteCollector>(NumberOfWorker);

			output = new StreamWriter(this.Output);
			
			object synObject = new object();

			for (int i = 0; i < NumberOfWorker; i++) {
				TestSuiteCollector w = new TestSuiteCollector(this, synObject);
				workers.Add(w);
				w.Finished += new EventHandler<TestSuiteCollectorFinishedArg>(workerFinished);
			}

			string baseURL = TestSuitURL.format(ItemPerPage) + QueryArguments;

			for (int page = StartingPage; page <= PageCount; page++) {
				// find a idle collector
				TestSuiteCollector collector = workers.Find(p => !p.Busy);
				if (collector == null)
					lock(synObject)					
						Monitor.Wait(synObject);
				else 
					collector.collect(baseURL, page);
			}

			// wait for all worker stops
			while (workers.FindIndex(p => p.Busy) >= 0)
				lock (synObject)
					Monitor.Wait(synObject);

			// stop all worker threads
			stopWorkers();
			
			output.Close();

			ProgressMonitor.stop();
			
			Console.log("Completed.");
			// show show statistics
			showStatistic();
		}

		private void showStatistic() {
			if (Console != null) {
				Console.writeLine("Items per page : {0}", ItemPerPage); 
				Console.writeLine("Processed      : {0} / {1} pages", ProcessedPages, PageCount != int.MaxValue ? Math.Max(ProcessedPages, PageCount) : 0);
				Console.writeLine("Test Result collected: {0:#,##0}", TestResultCount);
			}
		}

		void workerFinished(object sender, TestSuiteCollectorFinishedArg e) {
			if (e.Error != null)
				abort(e.Error);
			else {

				lock (this) {
					ProcessedPages++;
					TestResultCount += e.Rows.Count;
					if (PageCount == int.MaxValue) {
						PageCount = e.PageCount > 0 ? e.PageCount : e.Rows.Count > 0 ? int.MaxValue : ProcessedPages;
					}

					foreach (TestSuiteEntry entry in e.Rows) {
						output.WriteLine("{0},{1},{2},{3}", entry.Result, entry.SubmissionDate, entry.TestCaseID, entry.Branch);
					}
				}
				ProgressMonitor.update(ProcessedPages, PageCount);
				ProgressMonitor.Description = "{0} entries extracted.".format(TestResultCount);
				lock (e.SyncObject)
					Monitor.PulseAll(e.SyncObject);
			}
		}

		protected override void onError(Exception exception) {
			output.Close();
			stopWorkers();
			showStatistic();
			base.onError(exception);
		}

		private void stopWorkers() {
			if (workers != null)
				foreach (TestSuiteCollector worker in workers)
					worker.stop();
		}
	}

	class TestSuiteCollectorFinishedArg : EventArgs {
		public int PageCount { get; set; }
		public IList<TestSuiteEntry> Rows { get; set; }
		public Exception Error { get; set; }
		public object SyncObject { get; private set; }

		public TestSuiteCollectorFinishedArg(object syncObj) {
			this.SyncObject = syncObj;
			Rows = new List<TestSuiteEntry>();
		}
	}

	public enum TestSuiteEntryResult { Pass, Fail, Unclear, NotSpecify}

	public class TestSuiteEntry {
		public int ID { get; set; }
		public TestSuiteEntryResult Result { get; set; }
		public DateTime SubmissionDate { get; set; }
		public int TestCaseID { get; set; }
		public string Branch { get; set; }
	}

	class TestSuiteCollector {
		
		private object synObject;
		private string url = string.Empty;
		private object dataReady = new object();
		private int page;
		private FirefoxTestSuiteCollectorAction action;

		public TestSuiteCollector() {
		}

		public TestSuiteCollector(FirefoxTestSuiteCollectorAction action, object synObject) {
			this.synObject = synObject;
			WorkingThread = new Thread(process);
			WorkingThread.Start();
			this.action = action;
		}

		public event EventHandler<TestSuiteCollectorFinishedArg> Finished;
		
		public bool Busy { get; private set; }

		public Thread WorkingThread { get; private set; }

		/// <summary>
		/// Collect test suite data at specific URL, and return number of entried collected.
		/// </summary>
		/// <param name="url"></param>
		public void collect(string url, int page) {
			if (Busy)
				throw new Exception("This collector is busy.");
			if (!WorkingThread.IsAlive)
				throw new Exception("This collector is dead.");
			lock (dataReady) {
				this.url = url + (page > 1 ? "&page=" + page.ToString() : "");
				this.page = page;
				Busy = true;
				Monitor.PulseAll(dataReady);
			}
		}

		private void process() {
			try {
				while (true) {
					Busy = false;
					// wait for URL to be set
					if (url == string.Empty)
						lock (dataReady)
							Monitor.Wait(dataReady);
					// if URL is not set, but data is set to be ready, then terminate the process loop
					if (url == string.Empty)
						break;

					Busy = true;
					action.Console.writeLine("Process page: " + url);
					string data = FileExtension.readWebFile(url, "testsuite-{0}-{1}.cache-htm".format(action.ItemPerPage, page));

					int idx = data.IndexOf("<table class=\"test-results\">");
					if (idx > 0) {
						int eidx = data.IndexOf("</table>", idx);
						data = data.Substring(idx, eidx + "</table>".Length - idx + 1);
						parseReturnHtml(data);
					}
					// reset URL, and wait
					url = string.Empty;
				}
			}
			catch (Exception ex) {
				Busy = false;
				if (Finished != null)
					Finished(this, new TestSuiteCollectorFinishedArg(synObject) { Error = ex });
			}
		}

		public void testParseHtml() {
			string data = string.Empty;
			using (var f = new StreamReader("test-data2.htm"))
				data = f.ReadToEnd();
			int idx = data.IndexOf("<table class=\"test-results\">");
			if (idx > 0) {
				int eidx = data.IndexOf("</table>", idx);
				data = data.Substring(idx, eidx + "</table>".Length - idx + 1);
				parseReturnHtml(data);
			}
		}

		public void parseReturnHtml(string data) {
			TestSuiteCollectorFinishedArg result = new TestSuiteCollectorFinishedArg(synObject);

			Parser parser = new Parser(new Lexer(data));
			INode[] nodes = parser.ExtractAllNodesThatAre(typeof(TableTag));
			TableTag tbl = (TableTag)nodes[0];
			try {
				if (tbl.RowCount > 0) {
					// first row is navigation rows
					TableRow navRow = tbl.Rows[0];
					TableColumn col = navRow.Columns[0];

					for (int i = 0; i < col.ChildCount; i++ ) {
						INode child = col.GetChild(i);
						if (child is ATag) {
							ATag t = (ATag)child;
							if (string.Equals(t.Attributes["CLASS"], "pageLast")) {
								string href = (string)t.Attributes["HREF"];
								if (href != null) {
									int idx = href.IndexOf("page=");
									if (idx > 0)
										result.PageCount = href.Substring(idx + 5).toInt();
								}
							}
						}
					}
				}
				for (int i = 2; i < tbl.RowCount - 1; i++) {
					TableRow tr = tbl.Rows[i];
					TestSuiteEntry entry = new TestSuiteEntry();
					entry.Result = (tr.Attributes["CLASS"] as string).convertTo<TestSuiteEntryResult>();
					entry.SubmissionDate = DateTime.Parse(tr.Columns[0].StringText);
					entry.TestCaseID = tr.Columns[2].Children[0].ToPlainTextString().toInt();
					entry.Branch = tr.Columns[5].StringText;
					result.Rows.Add(entry);
				}
			}
			catch (Exception ex) {
				result.Error = ex;
			}
			Busy = false;
			if (Finished != null)
				Finished(this, result);
		}

		public void stop() {
			if (WorkingThread.ThreadState == System.Threading.ThreadState.Running)
				WorkingThread.Abort();
			else {
				url = string.Empty;
				lock (dataReady)
					Monitor.PulseAll(dataReady);
			}
		}
	}
}
